package com.blustc.detect.ane.androino;

import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FSKModule {

    private static int SAMPLING_FREQUENCY = 44100; //Hz
    private static int BAUD_RATE = 100;


    private static int SAMPLES_PER_BIT = SAMPLING_FREQUENCY / BAUD_RATE;

    // bit-high = 60 peaks
    // bit-low = 20 peaks
    private static final int HIGH_BIT_N_PEAKS = 60;
    private static final int LOW_BIT_N_PEAKS = 20;

    private static final double PEAK_AMPLITUDE_TRESHOLD = 500; // significant sample (not noise)
    private static final int NUMBER_SAMPLES_PEAK = 4;            // minimum number of significant samples to be considered a peak

    private static final int MINUMUM_NPEAKS = 400; // if lower it means that there is no signal/message

    private static final int BIT_HIGH_SYMBOL = 1;
    private static final int BIT_LOW_SYMBOL = 0;
    private static final int BIT_NONE_SYMBOL = -1;

    private static final int CARRIER_INTERVAL_ZERO_BITS = 6;

    private static final int RECEIVE_BYTE_NUM = 5;

    private static final String TAG = "FSKModule";

    private FSKModule() {
    }

    private static void debugInfo(String message) {
        Log.d(TAG, "FSKDEC:" + message);
    }


//-----------------------------------------
// DECODING FUNCTIONS
//-----------------------------------------

    public static byte[] decodeSound(short[] sound) {
        FSKModule m = new FSKModule();
        List<BitBean> bits = m.processSound2(sound);
//        for (BitBean bit : bits) {
//            debugInfo("value: " + bit.getValue() + " , peaks:" + bit.getNumOfpeaks() +", index:"+bit.getEndSampleIndex());
//        }
        return m.decodeUniqueMessage(bits);
    }

    private byte[] decodeUniqueMessage(List<BitBean> bits) {
        if(bits.size() <= RECEIVE_BYTE_NUM * 8) return null;
        // start bit
        int index = findStartBit(bits);
       // debugInfo("decodeUniqueMessage():start bit=" + index);
        if (index == -1) return null; // no start-bit detected

        byte[] correctedMessage = decodeUniqueMessageCorrected(bits, index);
        return correctedMessage;
    }

    private int findStartBit(List<BitBean> bits) {
        int startBitIndex = -1;
        for(int i=0; i<bits.size(); i++){
            if(bits.get(i).getValue() == BIT_NONE_SYMBOL){
                startBitIndex = i;
                break;
            }
        }
        if(bits.size()  > startBitIndex + RECEIVE_BYTE_NUM * 8){
            return startBitIndex + 1;
        }
        return -1;
    }

    private byte[] decodeUniqueMessageCorrected(List<BitBean> bits, int startBit) {
        int message = 0;
        byte[] receiveBytes = new byte[RECEIVE_BYTE_NUM];
        //debugInfo("decode corrected: message=" + message + ":" + Integer.toBinaryString(message));
        if (bits.size() >= startBit + RECEIVE_BYTE_NUM * 8) {
            for (int i = 0; i < RECEIVE_BYTE_NUM; i++) {
                receiveBytes[i] = bitToByte(bits, startBit);
                startBit += 8;
            }
        }
        return receiveBytes;
    }

    private byte bitToByte(List<BitBean> bits, int startIndex) {
        byte rxByte = 0;
        for (int i = startIndex; i < startIndex + 8; i++) {
            if (bits.get(i).getValue() == BIT_HIGH_SYMBOL) {
                rxByte |= (bits.get(i).getValue() & 1) << (7 - (i - startIndex));
            }
        }
        return rxByte;
    }


    private List<BitBean> processSound2(short[] sound) {
        List<SamplePeakBean> samplePeakBeanList = new ArrayList<SamplePeakBean>();

        calSamplePeakList(sound, samplePeakBeanList);
        PeakAreaEnum peakAreaEnum = PeakAreaEnum.UNKNOWN;
        List<BitBean> bits = new ArrayList<BitBean>();
        int samePeakNums = 0;
        int bitSampleIndex = 0;
        for (SamplePeakBean samplePeakBean : samplePeakBeanList) {
            PeakAreaEnum currentPeakAreaEnum = PeakAreaEnum.UNKNOWN;
            if (samplePeakBean.getNumberOfSamples() > NUMBER_SAMPLES_PEAK) {
                currentPeakAreaEnum = PeakAreaEnum.ONE;
            }
            if (samplePeakBean.getNumberOfSamples() > NUMBER_SAMPLES_PEAK * 3) {
                currentPeakAreaEnum = PeakAreaEnum.ZERO;
            }
            if (peakAreaEnum == PeakAreaEnum.UNKNOWN) {
                peakAreaEnum = currentPeakAreaEnum;
                continue;
            }
            if (samplePeakBean.getEndIndex() - bitSampleIndex > CARRIER_INTERVAL_ZERO_BITS * SAMPLES_PER_BIT) {
                bits.add(new BitBean(BIT_NONE_SYMBOL, samePeakNums, samplePeakBean.getEndIndex()));
                bitSampleIndex =  samplePeakBean.getEndIndex();
            }
            if (peakAreaEnum != currentPeakAreaEnum) {
                if (peakAreaEnum == PeakAreaEnum.ZERO && samePeakNums >= LOW_BIT_N_PEAKS -2) {
                    bits.add(new BitBean(BIT_LOW_SYMBOL, samePeakNums, samplePeakBean.getEndIndex()));
                    bitSampleIndex =  samplePeakBean.getEndIndex();
                } else if (peakAreaEnum == PeakAreaEnum.ONE && samePeakNums >= HIGH_BIT_N_PEAKS -2) {
                    bits.add(new BitBean(BIT_HIGH_SYMBOL, samePeakNums, samplePeakBean.getEndIndex()));
                    bitSampleIndex =  samplePeakBean.getEndIndex();
                }
                peakAreaEnum = currentPeakAreaEnum;
                samePeakNums = 0;
            } else {
                if (currentPeakAreaEnum == PeakAreaEnum.ZERO && samePeakNums >= LOW_BIT_N_PEAKS) {
                    bits.add(new BitBean(BIT_LOW_SYMBOL, samePeakNums, samplePeakBean.getEndIndex()));
                    bitSampleIndex =  samplePeakBean.getEndIndex();
                    samePeakNums = 0;
                } else if (currentPeakAreaEnum == PeakAreaEnum.ONE && samePeakNums >= HIGH_BIT_N_PEAKS) {
                    bits.add(new BitBean(BIT_HIGH_SYMBOL, samePeakNums, samplePeakBean.getEndIndex()));
                    bitSampleIndex =  samplePeakBean.getEndIndex();
                    samePeakNums = 0;
                }
                samePeakNums++;
            }
        }
        return bits;
    }

    private void calSamplePeakList(short[] sound, List<SamplePeakBean> samplePeakBeanList) {
        int index = 0;
        int numberSamplesGreaterThresdhold = 0;
        int sign = 0; // initialized at the first significant value

        do {
            short value = sound[index];
            //debugInfo("sound["+index+"]= " + value);
            if (Math.abs(value) > PEAK_AMPLITUDE_TRESHOLD)
                numberSamplesGreaterThresdhold++; //significant value
            // sign initialization: take the sign of the first significant value
            if (sign == 0 && numberSamplesGreaterThresdhold > 0)
                sign = value / Math.abs(value);
            boolean signChanged = false;
            if (sign < 0 && value > 0) signChanged = true;
            if (sign > 0 && value < 0) signChanged = true;

            if (signChanged && numberSamplesGreaterThresdhold > NUMBER_SAMPLES_PEAK) {
                samplePeakBeanList.add(new SamplePeakBean(index, numberSamplesGreaterThresdhold));
                sign = -1 * sign; //change sign
                numberSamplesGreaterThresdhold = 0;

            }
            index++;
        } while (index < sound.length);
    }


    public enum PeakAreaEnum {
        UNKNOWN, ZERO, ONE;
    }

    public class SamplePeakBean {
        private int endIndex;
        private int numberOfSamples;

        public SamplePeakBean(int endIndex, int numberOfSamples) {
            this.endIndex = endIndex;
            this.numberOfSamples = numberOfSamples;
        }
        public int getEndIndex() {
            return endIndex;
        }
        public int getNumberOfSamples() {
            return numberOfSamples;
        }

    }

    public class BitBean {
        private int value;
        private int numOfpeaks;
        private int endSampleIndex;

        public BitBean(int value, int numOfpeaks, int endSampleIndex) {
            this.value = value;
            this.numOfpeaks = numOfpeaks;
            this.endSampleIndex = endSampleIndex;
        }

        public int getValue() {
            return value;
        }
        public int getNumOfpeaks() {
            return numOfpeaks;
        }
        public int getEndSampleIndex() {  return endSampleIndex; }

    }

}
